JavaScript 的自由度讓我們能隨意的新增及更改物件屬性值,但是有些時候我們想要有些限制,以保持我們資料的正確性。比如說某個屬性的資料類別是數字,我們希望在設值之前能做檢查,拒絕不是數字的新值。
當然可以另外建立一個檢查的函式,不過 JavaScript 物件提供了 getter 和 setter 二種工具,能讓我們用物件屬性方式呼叫方法,使得程式碼更簡潔,不必再另外建立函式。
const ash = {
pokemons: ["Pikachu", "Bulbasaur", "Charmander", "Squirtle", "Butterfree", "Pidgey"],
get firstPokemon() {
return this.pokemons[0];
},
set firstPokemon(pokemon) {
this.pokemons[0] = pokemon;
}
};
console.log(ash.firstPokemon === "Pikachu"); // true
console.log(ash.firstPokemon = "Eevee");
console.log(ash.firstPokemon === "Eevee"); // true
console.log(ash.pokemons[0] === "Eevee"); // true
Ash 的夢想是成為世界上最偉大的寶可夢訓練大師,所以帶著寶可夢各處去挑戰,但是一個人最多只能帶六隻寶可夢。
Ash 出發時帶著pokemons: ["Pikachu", "Bulbasaur", "Charmander", "Squirtle", "Butterfree", "Pidgey"]
這六隻寶可夢,他可以將其中一隻放在第一位,寶可夢就會待在他的身旁。
這裡我們用了firstPokemon
屬性名稱,在前面用get
和set
關鍵字分別寫了二個函式,get firstPokemon
會回傳pokemons
陣列第一個的值,set firstPokemon
會將陣列第一個的值取代為參數的值。
使用上就和平常呼叫物件屬性一樣,想知道 Ash 帶了哪一隻寶可夢在身邊,用ash.firstPokemon
,今天 Ash 得到了新寶可夢 Eevee,喜新厭舊要換掉 Pikachu,用ash.firstPokemon = "Eevee"
。
這樣的語法是不是很簡單呢?如果我們採用的是類別的寫法,也只要在類別方法名稱前面加上get
或set
。上面的範例可以改寫成如下。
class Trainer {
constructor() {
this.pokemons = ["Pikachu", "Bulbasaur", "Charmander", "Squirtle", "Butterfree", "Pidgey"];
}
get firstPokemon() {
return this.pokemons[0];
}
set firstPokemon(pokemon) {
this.pokemons[0] = pokemon;
}
}
const ash = new Trainer();
在物件屬性上設立get
和set
有個缺點,就是它無法運用函式的閉包特性,物件屬性還是可以被外界存取ash.pokemons
。幸好我們還有另一個方法,用前幾天提到過的Object.defineProperty
。
Object.defineProperty
能讓我們建立物件屬性並且設定屬性的性質,它可以代入三個參數,第一個是要建立屬性的物件,第二個是屬性名稱,第三個參數是一個 descriptor 物件,裡面可以帶有get
和set
屬性,分別會在物件屬性取用及設定時呼叫。
// 建立寶可夢訓練師的 constructor function
function Trainer() {
let _inventorySize = 10;
let _inventory = ["Pokeball", "Pokeball", "Pokeball", "Potion", "Potion", "Razz Berry"];
Object.defineProperty(this, "inventorySize", {
get: () => _inventorySize,
set: value => {
if(!Number.isInteger(value)) {
console.log("Inventory size should be a number!");
return;
}
_inventorySize = value;
}
});
Object.defineProperty(this, "inventory", {
get: () => _inventory
});
Object.defineProperty(this, "getItem", {
set: items => {
if(!Array.isArray(items)) {
console.log("Items should be an array!");
return;
} else if(items.length + _inventory.length > _inventorySize) {
console.log("Your inventory is out of space, buy a larger bag!");
return;
}
_inventory = _inventory.concat(items);
}
});
Object.defineProperty(this, "useItem", {
set: item => {
let i = _inventory.indexOf(item);
if(i < 0) {
console.log("You don't have this item in your inventory!");
return;
}
_inventory.splice(i, 1);
}
});
}
// Ash 的夢想是成為寶可夢訓練大師
const ash = new Trainer();
// Ash 使用攜帶的物品
console.log(ash.inventory);
// ["Pokeball", "Pokeball", "Pokeball", "Potion", "Potion", "Razz Berry"]
ash.useItem = "Potion";
console.log(ash.inventory);
// ["Pokeball", "Pokeball", "Pokeball", "Potion", "Razz Berry"]
ash.getItem = ["Great Ball", "Sun Stone"];
// ["Pokeball", "Pokeball", "Pokeball", "Potion", "Razz Berry", "Great Ball", "Sun Stone"]
// Ash 想要擴充他的背包容量
console.log(ash.inventorySize);
// 10
ash.inventorySize = "Unlimited";
// "Inventory size should be a number!"
console.log(ash.inventorySize);
// 10
ash.inventorySize = 15;
console.log(ash.inventorySize);
// 15
Ash 從真新鎮出發時,大木博士給他一個可以裝十件物品的背包,和一些基本的用品。_inventorySize
和_inventory
都是私有變數,只有透過我的設計的函式才能更改。
Ash 可以用ash.inventorySize
得到他的背包容量,當他買了新背包可以擴充容量,容量的值我們設定是整數,避免玩家用金手指破壞遊戲規則。
在物品使用上我們建了二個屬性,當他得到新物品時會用ash.getItem
先檢查新物品數量能不能放進背包,不行的話會顯示錯誤訊息。夠放的話就會把新物品收進_inventory
私有變數內。當使用物品ash.useItem
時,會試著在_inventory
裡找第一個符合物品名稱的 index,沒找到會顯示錯誤訊息,有找到會將該物品從陣列中移除。
不過getter
和setter
的能力不只有這樣,我們能用它產生自動化的屬性,這個屬性會根據既有屬性的值,在每次取值的時候依照設定的計算過程產生。
const ash = {
firstName: "Ash",
lastName: "Ketchum",
age: 10,
hometown: "Pallet Town",
gender: "male",
dream: "become the best pokemon trainer in the world",
get biography() {
const refer = (function(gender, age){
if(gender === "male"){
return age < 20 ? "boy" : "man";
} else {
return age < 20 ? "girl" : "woman";
}
})(this.gender, this.age);
return `${this.firstName} ${this.lastName}, a ${refer} from ${this.hometown}, has a dream to ${this.dream}!`;
}
};
// 我們能依照 Ash 填寫的寶可夢訓練師基本資料,自動生成他的自我介紹!
console.log(ash.biography);
// Ash Ketchum, a boy from Pallet Town, has a dream to become the best pokemon trainer in the world!
這樣的屬性叫做 computed property,如果你用過 Vue.js 的話應該對這種屬性不陌生,事實上 Vue 就是利用getter
來做到的。